package implementation1.app;

import exceptions.CreatingProcessException;
import implementation1.app.streams.StdStream;
import implementation1.data.states.*;
import implementation1.gui.MainWindow;
import interfaces.app.IProcess;
import interfaces.app.IStream;
import interfaces.data.IHandle;
import interfaces.data.IStatus;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collection;

/**
 * Created: 15.11.11
 *
 * @author Martin Štulc (<a href="mailto:martin.stulc@gmail.com">martin.stulc@gmail.com</a>)
 * @version 1.0
 */
public class InitProcess implements IProcess
{
    /**
     * Default timeout for quit the process.
     */
    public final static int DEFAULT_TIMEOUT_TO_QUIT = 5000;

    /**
     * A parent process
     */
    protected IProcess parentProcess;
    /**
     * List with child's processes.
     */
    protected Collection<IProcess> children = new ArrayList<IProcess>();
    /**
     * Initialization data  of process
     */
    protected IHandle handle;
    /**
     * PID
     */
    protected int pid;
    /**
     * Status of process
     */
    protected IStatus status;
    /**
     * Reference to thread of process
     */
    public Thread threadOfProcess;
    /**
     * List of extra-opened streams.
     */
    protected Collection<IStream> openedStreamsOfProcess;
    /**
     * Contains PID for next process. If PID is Long.MAXVALUE,then special function finds first free number from 0.
     */
    public static int PID_COUNTER = 0;

    /**
     * Default constructor.
     *
     * @param handle initialization parameters for process
     */
    public InitProcess(IHandle handle)
    {
        this.parentProcess = null;
        this.handle = handle;

        status = new NewStatus();

        threadOfProcess = null;
        openedStreamsOfProcess = new ArrayList<IStream>();

        pid = getPIDForProcess();
    }

    /**
     * Get PID for new process.
     *
     * @return PID for process
     */
    protected int getPIDForProcess()
    {
        if (InitProcess.PID_COUNTER == Integer.MAX_VALUE)
        {
            IProcess root = getRootProcess();

            for (int pid = 0; pid < Integer.MAX_VALUE; pid++)
            {
                if (!PIDIsUsed(root, pid))
                {
                    return pid;
                }
            }

            return -1;
        }

        return InitProcess.PID_COUNTER++;
    }

    /**
     * Checks if required PID is already used.
     *
     * @param startProcess a reference to process from which will be searched the required PID
     * @param pid          the required PID
     * @return TRUE - is used, otherwise FALSE
     */
    private boolean PIDIsUsed(IProcess startProcess, int pid)
    {
        if (startProcess.getPid() == pid)
        {
            return true;
        }

        for (IProcess childP : startProcess.getChildren())
        {
            if (PIDIsUsed(childP, pid))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Return a status of the process.
     *
     * @return the process status
     */
    @Override
    public IStatus getStatus()
    {
        return status;
    }

    /**
     * Return a handle of the process.
     *
     * @return the process handle
     */
    @Override
    public IHandle getHandle()
    {
        return handle;
    }

    /**
     * Return PID of the process.
     *
     * @return the process PID
     */
    @Override
    public int getPid()
    {
        return pid;
    }

    /**
     * Create a child process. Use ProcessCreator class for that.
     *
     * @param handle handle of process
     * @return created process
     */
    @Override
    public IProcess createChildProcess(IHandle handle) throws CreatingProcessException
    {
        ProcessCreator processCreator = new ProcessCreator();

        IProcess newChild = processCreator.createProcess(this, handle);
        children.add(newChild);
        return newChild;
    }

    /**
     * Start the process.
     *
     * @return true if start was successful; otherwise false
     */
    @Override
    public boolean startProcess()
    {
        if (threadOfProcess != null)
        {
            return false;
        }

        threadOfProcess = new Thread(this, handle.getCommandName());
        threadOfProcess.start();

        return true;
    }

    /**
     * Wait to child which has the pid.
     *
     * @param pid the pid of the child
     */
    @Override
    public void waitToChild(int pid)
    {
        IProcess childWithPid = null;

        for (IProcess child : children)
        {
            if (child.getPid() == pid)
            {
                childWithPid = child;
                break;
            }
        }

        if (childWithPid == null || childWithPid.hasEndStatus())
        {
            return;
        }

        status = new WaitingStatus();

        synchronized (childWithPid)
        {
            try
            {
                childWithPid.wait();
            } catch (InterruptedException e)
            {
                status = new TerminatedStatus();
            }
        }
        if (!hasEndStatus())
        {
            status = new RunningStatus();
        }
    }

    /**
     * Wait to all children.
     */
    @Override
    public void waitToAllChildren()
    {
        for (IProcess child : children)
        {
            waitToChild(child.getPid());
        }
    }

    /**
     * Regular quiting of the process. Send quit to all children and waiting to them.
     *
     * @return true if process was exited; otherwise false (probably must call terminate)
     */
    @Override
    public boolean quit()
    {
        status = new FinishedStatus();

        Collection<IProcess> childrenTemp = new ArrayList<IProcess>(getChildren());

        for (IProcess child : childrenTemp)
        {
            child.quit();
        }

        for (IStream stream : getOpenedStreams())
        {
            if (stream instanceof StdStream)
            {
                ((StdStream) stream).closeShellWindow();
            }
            Global.closeStreamQuietly(stream);
        }

        synchronized (this)
        {
            this.notifyAll();
        }
        return true;
    }

    /**
     * Terminate the process and all his children.
     */
    @Override
    public void terminate()
    {
        status = new TerminatedStatus();

        Collection<IProcess> childrenTemp = new ArrayList<IProcess>(getChildren());

        for (IProcess child : childrenTemp)
        {
            child.terminate();
        }

        for (IStream stream : getOpenedStreams())
        {
            if (stream instanceof StdStream)
            {
                ((StdStream) stream).closeShellWindow();
            }
            Global.closeStreamQuietly(stream);
        }

        if (!threadOfProcess.isAlive())
        {
            threadOfProcess.interrupt();
        }

        synchronized (this)
        {
            this.notifyAll();
        }
    }

    /**
     * Return a parent process.
     *
     * @return the parent process
     */
    @Override
    public IProcess getParent()
    {
        return parentProcess;
    }

    /**
     * Return a root process.
     *
     * @return the root process.
     */
    @Override
    public IProcess getRootProcess()
    {
        if (parentProcess == null)
        {
            return this;
        }

        IProcess parentRoot = parentProcess;

        while (parentRoot.getParent() != null)
        {
            parentRoot = parentRoot.getParent();
        }

        return parentRoot;
    }

    /**
     * A collection of opened streams of the process.
     *
     * @return the opened streams
     */
    @Override
    public Collection<IStream> getOpenedStreams()
    {
        Collection<IStream> openedStreams = new ArrayList<IStream>();

        for (IStream stream : openedStreamsOfProcess)
        {
            if (stream.isOpen())
            {
                openedStreams.add(stream);
            }
        }

        return openedStreams;
    }

    /**
     * A collection of opened streams of the process and all children.
     *
     * @return the opened streams
     */
    @Override
    public Collection<IStream> getOpenedStreamsRecursively()
    {
        Collection<IStream> openedStreams = new ArrayList<IStream>();

        openedStreams.addAll(openedStreamsOfProcess);

        for (IProcess child : children)
        {
            openedStreams.addAll(child.getOpenedStreamsRecursively());
        }

        return openedStreams;
    }

    /**
     * A collection of opened streams of the process which are opened exclusively.
     *
     * @return the opened exclusive streams
     */
    @Override
    public Collection<IStream> getOpenedExclusiveStream()
    {
        Collection<IStream> openedExclStreams = new ArrayList<IStream>();

        for (IStream stream : getOpenedStreams())
        {
            if (stream.isExclusive() && stream.isOpen())
            {
                openedExclStreams.add(stream);
            }
        }

        return openedExclStreams;
    }

    /**
     * A collection of opened streams of the process and all children which are opened exclusively.
     *
     * @return the opened exclusive streams
     */
    @Override
    public Collection<IStream> getOpenedExclusiveStreamRecursively()
    {
        Collection<IStream> openedExclStreams = new ArrayList<IStream>();

        openedExclStreams.addAll(getOpenedExclusiveStream());

        for (IProcess child : children)
        {
            openedExclStreams.addAll(child.getOpenedExclusiveStreamRecursively());
        }

        return openedExclStreams;
    }

    /**
     * Return true if the process ended.
     *
     * @return true if the process ended; otherwise false
     */
    @Override
    public Boolean hasEndStatus()
    {
        return (status instanceof FinishedStatus) || (status instanceof TerminatedStatus);
    }

    /**
     * Return collection of the children of the process.
     *
     * @return collection of the children
     */
    @Override
    public Collection<IProcess> getChildren()
    {
        return children;
    }

    /**
     * Return collection of all children and their children of the process.
     *
     * @return collection of all children and their children
     */
    @Override
    public Collection<IProcess> getChildrenRecursively()
    {
        Collection<IProcess> allChildren = new ArrayList<IProcess>();
        allChildren.add(this);

        for (IProcess child : children)
        {
            allChildren.addAll(child.getChildrenRecursively());
        }

        return allChildren;
    }

    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p/>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    @Override
    public void run()
    {
        status = new RunningStatus();
        final JFrame mw = new MainWindow(this);

        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                mw.setVisible(true);
            }
        });

        try
        {
            synchronized (this)
            {
                this.status = new WaitingStatus();
                this.wait();
            }

            status = new FinishedStatus();

        } catch (InterruptedException e)
        {
            status = new TerminatedStatus();
        }

        mw.dispose();
        System.exit(0);
    }

    @Override
    public String toString()
    {
        return "init";
    }
}
